// =====================================================================
//
//  This file is part of the Microsoft Dynamics CRM SDK code samples.
//
//  Copyright (C) Microsoft Corporation.  All rights reserved.
//
//  This source code is intended only as a supplement to Microsoft
//  Development Tools and/or on-line documentation.  See these other
//  materials for detailed information regarding Microsoft code samples.
//
//  THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
//  KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
//  IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
//  PARTICULAR PURPOSE.
//
// =====================================================================
#region

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.Xml.Linq;
using Microsoft.Xrm.Sdk.Client.Protocols.Policy;
using Microsoft.Xrm.Sdk.Client.Protocols.WSDL;


#endregion

namespace Microsoft.Xrm.Sdk.Client
{
	/// <summary>
	/// 	Handles retrieving/making use of service metadata information.
	/// </summary>
	public class ServiceEndpointMetadata
	{
		private IssuerEndpointDictionary _serviceIssuerEndpoints = new IssuerEndpointDictionary();

		private ServiceEndpointMetadata()
		{
		}

		public ServiceEndpointMetadata(Uri serviceUri)
		{
			ServiceUri = serviceUri;
		}

		public EndpointAddress ServiceIssuerMetadataEndpoint { get; private set; }
		public Uri ServiceUri { get; private set; }
		public AuthenticationPolicy AuthenticationPolicy { get; set; }
		public SecureTokenServicePolicy TokenPolicy { get; set; }


		public PolicyConfiguration PolicyConfiguration { get; private set; }

		/// <summary>
		/// 	Identifies whether the constructed service is using Claims (Federation) authentication or legacy AD/RPS.
		/// </summary>
		public AuthenticationProviderType AuthenticationType
		{
			get
			{
				if (PolicyConfiguration is WindowsPolicyConfiguration)
				{
					return AuthenticationProviderType.ActiveDirectory;
				}
				if (PolicyConfiguration is ClaimsPolicyConfiguration)
				{
					return AuthenticationProviderType.Federation;
				}
				if (PolicyConfiguration is LiveIdPolicyConfiguration)
				{
					return AuthenticationProviderType.LiveId;
				}
				if (PolicyConfiguration is OnlineFederationPolicyConfiguration)
				{
					return AuthenticationProviderType.OnlineFederation;
				}
				return AuthenticationProviderType.None;
			}
		}

		public Boolean ClaimsEnabledService
		{
			get
			{
				return (AuthenticationType == AuthenticationProviderType.Federation ||
						AuthenticationType == AuthenticationProviderType.OnlineFederation);
			}
		}

		public void Load()
		{
			WsdlReader wsdlReader = new WsdlReader();
			wsdlReader.MetadataLoaded += ServiceEndpointMetadataLoaded;

			Uri wsdlUri = new Uri(ServiceUri.AbsoluteUri + "?wsdl");
			wsdlReader.Retrieve(wsdlUri);
		}

		/// <summary>
		/// 	If there is no binding, there is nothing to do.  Otherwise, import the XRM Policy elements.
		/// </summary>
		private void SetAuthenticationConfiguration()
		{
			if (AuthenticationPolicy != null)
			{
				if (AuthenticationPolicy.PolicyElements.ContainsKey("AuthenticationType"))
				{
					String type = AuthenticationPolicy.PolicyElements["AuthenticationType"];
					if (!String.IsNullOrEmpty(type))
					{
						// If the AuthenticationProviderType is invalid, allow the exception.
						AuthenticationProviderType authType =
							(AuthenticationProviderType)Enum.Parse(typeof(AuthenticationProviderType), type, true);
						switch (authType)
						{
							case AuthenticationProviderType.LiveId:
								PolicyConfiguration = new LiveIdPolicyConfiguration(this);
								break;
							case AuthenticationProviderType.OnlineFederation:
								PolicyConfiguration = new OnlineFederationPolicyConfiguration(this);
								foreach (var onlineProvider in ((OnlineFederationPolicyConfiguration)PolicyConfiguration).OnlineProviders.Values)
								{
									AddLiveIssuerEndpointsToCrossRealmIssuers(onlineProvider);
								}
								break;
							case AuthenticationProviderType.Federation:
								PolicyConfiguration = new ClaimsPolicyConfiguration(this);
								break;
							default:
								PolicyConfiguration = new WindowsPolicyConfiguration(this);
								break;
						}
						return;
					}
				}
			}
		}

		/// <summary>
		/// Contains the STS IssuerEndpoints as determined dynamically by calls to AuthenticateCrossRealm.
		/// </summary>
		public CrossRealmIssuerEndpointCollection CrossRealmIssuerEndpoints
		{
			get { return crossRealmIssuerEndpoints; }
			internal set { crossRealmIssuerEndpoints = value; }
		}
		private CrossRealmIssuerEndpointCollection crossRealmIssuerEndpoints = new CrossRealmIssuerEndpointCollection();

		private void AddLiveIssuerEndpointsToCrossRealmIssuers(IdentityProviderTrustConfiguration identityProviderTrustConfiguration)
		{
			var endpoints = ServiceMetadataUtility.RetrieveLiveIdIssuerEndpoints(identityProviderTrustConfiguration);
			if (endpoints != null)
			{
				CrossRealmIssuerEndpoints[identityProviderTrustConfiguration.Endpoint.GetServiceRoot()] = endpoints;
			}
		}
		/// <summary>
		/// Contains the list of urls and binding information required in order to make a call to a WCF service.  If the service is configured
		/// for On-Premise use only, then the endpoint(s) contained within will NOT require the use of an Issuer Endpoint on the binding.
		/// 
		/// If the service is configured to use Claims (Federation,) then the binding on the service endpoint MUST be configured to use
		/// the appropriate Issuer Endpoint, i.e., UserNamePassword, Kerberos, etc.
		/// </summary>
		public ServiceEndpointDictionary ServiceEndpoints { get; internal set; }

		/// <summary>
		/// The following property contains the urls and binding information required to use a configured Secure Token Service (STS)
		/// for issuing a trusted token that the service endpoint will trust for authentication.
		/// 
		/// The available endpoints can vary, depending on how the administrator of the STS has configured the server, but may include 
		/// the following authentication methods:
		/// 
		/// 1.  UserName and Password
		/// 2.  Kerberos
		/// 3.  Certificate
		/// 4.  Asymmetric Token
		/// 5.  Symmetric Token
		/// </summary>
		public IssuerEndpointDictionary IssuerEndpoints
		{
			get { return _serviceIssuerEndpoints; }
			internal set { _serviceIssuerEndpoints = value; }
		}

		private void ServiceEndpointMetadataLoaded(object sender, MetadataLoadedEventArgs e)
		{
			foreach (KeyValuePair<Uri, WsdlDocument> keyValuePair in e.Documents)
			{
				XDocument document = keyValuePair.Value.Document;
				if (keyValuePair.Value.Error != null)
				{
					FireMetadataLoadEvent(keyValuePair.Value.Error);
				}
				else
				{
					XrmPolicyManager policyManager = new XrmPolicyManager();

					if (policyManager.CanImport(document))
					{
						AuthenticationPolicy = policyManager.ImportPolicy(document) as AuthenticationPolicy;
						TokenPolicy = policyManager.ImportPolicy(document) as SecureTokenServicePolicy;
						SetAuthenticationConfiguration();
						if (ClaimsEnabledService)
						{
							ServiceIssuerMetadataEndpoint =
								new EndpointAddress(PolicyConfiguration.SecureTokenServiceTrustEndpoint);

							RetrieveIssuerEndpoints(new Uri(PolicyConfiguration.SecureTokenServiceTrustEndpoint));
						}
						else if (AuthenticationType == AuthenticationProviderType.LiveId)
						{

							IssuerEndpoint issuer =
									RetrieveDefaultIssuerEndpoint(
										new EndpointAddress(PolicyConfiguration.SecureTokenServiceTrustEndpoint));
							IssuerEndpoints.Add(issuer.CredentialType.ToString(), issuer);

							FireMetadataLoadEvent();
						}
						else
						{
							//Nothing to do for AD
							FireMetadataLoadEvent(new NotSupportedException("Unsupported deployment type"));
						}
					}
				}
			}
		}

		internal void RetrieveIssuerEndpoints(Uri issuerUri)
		{
			//First get the wsdl from the STS
			if (issuerUri != null)
			{
				WsdlReader wsdlReader = new WsdlReader();
				wsdlReader.MetadataLoaded += IssuerEndpointMetadataLoaded;
				wsdlReader.Retrieve(issuerUri);
			}
		}

		public IssuerEndpoint RetrieveDefaultIssuerEndpoint(EndpointAddress issuerAddress)
		{
			if (issuerAddress != null)
			{
				//Default to username
				return new IssuerEndpoint
				{
					CredentialType = TokenServiceCredentialType.Username,
					IssuerAddress = issuerAddress,
				};
			}
			return null;
		}


		private void IssuerEndpointMetadataLoaded(object sender, MetadataLoadedEventArgs e)
		{
			foreach (KeyValuePair<Uri, WsdlDocument> keyValuePair in e.Documents)
			{
				if (keyValuePair.Value.Error != null)
				{
					FireMetadataLoadEvent(keyValuePair.Value.Error);
				}
				else
				{
					XDocument document = keyValuePair.Value.Document;

					WSTrustPolicyManager policyManager = new WSTrustPolicyManager();

					if (policyManager.CanImport(document))
					{
						WSTrustPolicy trustPolicy = policyManager.ImportPolicy(document) as WSTrustPolicy;
						if (trustPolicy != null)
						{
							foreach (KeyValuePair<string, TokenPolicy> key in trustPolicy.TokenPolicies)
							{
								IssuerEndpoint endpoint = new IssuerEndpoint
															  {
																  IssuerAddress = key.Value.EndpointAddress,
																  TrustVersion = key.Value.TrustVersion,
																  IssuerMetadataAddress = ServiceIssuerMetadataEndpoint,
																  CredentialType = key.Value.CredentialType
															  };
								IssuerEndpoints.Add(key.Value.CredentialType.ToString(), endpoint);
							}
							FireMetadataLoadEvent();
						}
					}
				}
			}
		}

		private void FireMetadataLoadEvent(Exception exception = null)
		{
			if (exception == null)
			{
				ServiceMetadataLoadedEventArgs args = new ServiceMetadataLoadedEventArgs(this, null, false, null);
				OnMetadataLoaded(args);
			}
			else
			{
				ServiceMetadataLoadedEventArgs args = new ServiceMetadataLoadedEventArgs(this, exception, false, null);
				OnMetadataLoadFailure(args);
			}
		}

		private void OnMetadataLoaded(ServiceMetadataLoadedEventArgs args)
		{
			if (MetadataLoaded != null)
			{
				MetadataLoaded(this, args);
			}
		}

		private void OnMetadataLoadFailure(ServiceMetadataLoadedEventArgs args)
		{
			if (MetadataLoadFailure != null)
			{
				MetadataLoadFailure(this, args);
			}
		}

		public event EventHandler<ServiceMetadataLoadedEventArgs> MetadataLoaded;


		public sealed class ServiceEndpointDictionary : Dictionary<String, ServiceEndpoint>
		{
			public ServiceEndpointDictionary()
			{
			}
		}

		public event EventHandler<ServiceMetadataLoadedEventArgs> MetadataLoadFailure;
	}

	public class ServiceMetadataLoadedEventArgs : AsyncCompletedEventArgs
	{
		public ServiceMetadataLoadedEventArgs(ServiceEndpointMetadata serviceEndpointMetadata, Exception exception, bool cancelled, object userState) :
			base(exception, cancelled, userState)
		{
			this.serviceEndpointMetadata = serviceEndpointMetadata;
		}
		public ServiceEndpointMetadata ServiceEndpointMetadata
		{
			get
			{
				base.RaiseExceptionIfNecessary();
				return serviceEndpointMetadata;
			}
		}
		private ServiceEndpointMetadata serviceEndpointMetadata;
	}

	internal static class Extensions
	{
		internal static Uri GetServiceRoot(this Uri url)
		{
			ClientExceptionHelper.ThrowIfNull(url, "url");

			UriBuilder uriBuilder = new UriBuilder(url);
			uriBuilder.Query = String.Empty;
			uriBuilder.Path = String.Empty;

			return uriBuilder.Uri;
		}
	}

}
